﻿using System;
using System.ComponentModel;

namespace Microsoft.Xna.Framework
{
    /// <summary>
    /// Stores an arbitrary collection of 2D CurveKey points, and provides methods for evaluating features of the curve they define.
    /// </summary>
    [Serializable]
    public class Curve
    {
        #region Private Fields
        private CurveKeyCollection keys;
        private CurveLoopType postLoop;
        private CurveLoopType preLoop;
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets a value indicating whether the curve is constant.
        /// </summary>
        public bool IsConstant
        {
            get { return this.keys.Count <= 1; }
        }
        /// <summary>
        /// The points that make up the curve.
        /// </summary>
        public CurveKeyCollection Keys
        {
            get { return this.keys; }
        }
        /// <summary>
        /// Specifies how to handle weighting values that are greater than the last control point in the curve.
        /// </summary>
        public CurveLoopType PostLoop
        {
            get { return this.postLoop; }
            set { this.postLoop = value; }
        }
        /// <summary>
        /// Specifies how to handle weighting values that are less than the first control point in the curve.
        /// </summary>
        public CurveLoopType PreLoop
        {
            get { return this.preLoop; }
            set { this.preLoop = value; }
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Initializes a new instance of Curve.
        /// </summary>
        public Curve()
        {
            this.keys = new CurveKeyCollection();
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Creates a copy of the Curve.
        /// </summary>
        public Curve Clone()
        {
            Curve curve = new Curve();

            curve.keys = this.keys.Clone();
            curve.preLoop = this.preLoop;
            curve.postLoop = this.postLoop;

            return curve;
        }
        /// <summary>
        /// Computes a specified type of TangentIn and a specified type of TangentOut for a given CurveKey.
        /// </summary>
        /// <param name="keyIndex">The index of the CurveKey for which to compute tangents (in the Keys collection of the Curve).</param>
        /// <param name="tangentInType">The type of TangentIn to compute (one of the types specified in the CurveTangent enumeration).</param>
        /// <param name="tangentOutType">The type of TangentOut to compute (one of the types specified in the CurveTangent enumeration).</param>
        public void ComputeTangent(int keyIndex, CurveTangent tangentInType, CurveTangent tangentOutType)
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// Computes both the TangentIn and the TangentOut for a CurveKey specified by its index.
        /// </summary>
        /// <param name="keyIndex">The index of the CurveKey for which to compute tangents (in the Keys collection of the Curve).</param>
        /// <param name="tangentType">The type of tangents to compute (one of the types specified in the CurveTangent enumeration).</param>
        public void ComputeTangent(int keyIndex, CurveTangent tangentType)
        {
            this.ComputeTangent(keyIndex, tangentType, tangentType);
        }
        /// <summary>
        /// Computes all tangents for all CurveKeys in this Curve, using different tangent types for TangentOut and TangentIn.
        /// </summary>
        /// <param name="tangentInType">The type of TangentIn to compute (one of the types specified in the CurveTangent enumeration).</param>
        /// <param name="tangentOutType">The type of TangentOut to compute (one of the types specified in the CurveTangent enumeration).</param>
        public void ComputeTangents(CurveTangent tangentInType, CurveTangent tangentOutType)
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// Computes all tangents for all CurveKeys in this Curve, using a specified tangent type for both TangentIn and TangentOut.
        /// </summary>
        /// <param name="tangentType">The type of TangentOut and TangentIn to compute (one of the types specified in the CurveTangent enumeration).</param>
        public void ComputeTangents(CurveTangent tangentType)
        {
            this.ComputeTangents(tangentType, tangentType);
        }
        /// <summary>
        /// Finds the value at a position on the Curve.
        /// </summary>
        /// <param name="position">The position on the Curve.</param>
        public float Evaluate(float position)
        {
            CurveKey first = keys[0];
            CurveKey last = keys[keys.Count - 1];

            if (position < first.Position)
            {
                switch (this.PreLoop)
                {
                    case CurveLoopType.Constant:
                        //constant
                        return first.Value;

                    case CurveLoopType.Linear:
                        // linear y = a*x +b with a tangeant of last point
                        return first.Value - first.TangentIn * (first.Position - position);

                    case CurveLoopType.Cycle:
                        //start -> end / start -> end
                        int cycle = GetNumberOfCycle(position);
                        float virtualPos = position - (cycle * (last.Position - first.Position));
                        return GetCurvePosition(virtualPos);

                    case CurveLoopType.CycleOffset:
                        //make the curve continue (with no step) so must up the curve each cycle of delta(value)
                        cycle = GetNumberOfCycle(position);
                        virtualPos = position - (cycle * (last.Position - first.Position));
                        return (GetCurvePosition(virtualPos) + cycle * (last.Value - first.Value));

                    case CurveLoopType.Oscillate:
                        //go back on curve from end and target start 
                        // start-> end / end -> start
                        cycle = GetNumberOfCycle(position);
                        if (0 == cycle % 2f)//if pair
                            virtualPos = position - (cycle * (last.Position - first.Position));
                        else
                            virtualPos = last.Position - position + first.Position + (cycle * (last.Position - first.Position));
                        return GetCurvePosition(virtualPos);
                }
            }
            else if (position > last.Position)
            {
                int cycle;
                switch (this.PostLoop)
                {
                    case CurveLoopType.Constant:
                        //constant
                        return last.Value;

                    case CurveLoopType.Linear:
                        // linear y = a*x +b with a tangeant of last point
                        return last.Value + first.TangentOut * (position - last.Position);

                    case CurveLoopType.Cycle:
                        //start -> end / start -> end
                        cycle = GetNumberOfCycle(position);
                        float virtualPos = position - (cycle * (last.Position - first.Position));
                        return GetCurvePosition(virtualPos);

                    case CurveLoopType.CycleOffset:
                        //make the curve continue (with no step) so must up the curve each cycle of delta(value)
                        cycle = GetNumberOfCycle(position);
                        virtualPos = position - (cycle * (last.Position - first.Position));
                        return (GetCurvePosition(virtualPos) + cycle * (last.Value - first.Value));

                    case CurveLoopType.Oscillate:
                        //go back on curve from end and target start 
                        // start-> end / end -> start
                        cycle = GetNumberOfCycle(position);
                        virtualPos = position - (cycle * (last.Position - first.Position));
                        if (0 == cycle % 2f)//if pair
                            virtualPos = position - (cycle * (last.Position - first.Position));
                        else
                            virtualPos = last.Position - position + first.Position + (cycle * (last.Position - first.Position));
                        return GetCurvePosition(virtualPos);
                }
            }

            //in curve
            return GetCurvePosition(position);
        }
        #endregion

        #region Private Methods
        private int GetNumberOfCycle(float position)
        {
            float cycle = (position - keys[0].Position) / (keys[keys.Count - 1].Position - keys[0].Position);
            if (cycle < 0f)
                cycle--;
            return (int)cycle;
        }

        private float GetCurvePosition(float position)
        {
            //only for position in curve
            CurveKey prev = this.keys[0];
            CurveKey next;
            for (int i = 1; i < this.keys.Count; i++)
            {
                next = this.Keys[i];
                if (next.Position >= position)
                {
                    if (prev.Continuity == CurveContinuity.Step)
                    {
                        if (position >= 1f)
                        {
                            return next.Value;
                        }
                        return prev.Value;
                    }
                    float t = (position - prev.Position) / (next.Position - prev.Position);//to have t in [0,1]
                    float ts = t * t;
                    float tss = ts * t;
                    //After a lot of search on internet I have found all about spline function
                    // and bezier (phi'sss ancien) but finaly use hermite curve 
                    //http://en.wikipedia.org/wiki/Cubic_Hermite_spline
                    //P(t) = (2*t^3 - 3t^2 + 1)*P0 + (t^3 - 2t^2 + t)m0 + (-2t^3 + 3t^2)P1 + (t^3-t^2)m1
                    //with P0.value = prev.value , m0 = prev.tangentOut, P1= next.value, m1 = next.TangentIn
                    return (2 * tss - 3 * ts + 1f) * prev.Value + (tss - 2 * ts + t) * prev.TangentOut + (3 * ts - 2 * tss) * next.Value + (tss - ts) * next.TangentIn;
                }
                prev = next;
            }
            return 0f;
        }
        #endregion
    }
}
